Passed
Push — master ( 6ca9b2...ebd43e )
by Rafael S.
04:26
created

WaveFileCreator.bitDepthFromFmt_   A

Complexity

Conditions 4

Size

Total Lines 11
Code Lines 9

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 9
dl 0
loc 11
rs 9.95
c 0
b 0
f 0
cc 4
1
/*
2
 * Copyright (c) 2017-2019 Rafael da Silva Rocha.
3
 *
4
 * Permission is hereby granted, free of charge, to any person obtaining
5
 * a copy of this software and associated documentation files (the
6
 * "Software"), to deal in the Software without restriction, including
7
 * without limitation the rights to use, copy, modify, merge, publish,
8
 * distribute, sublicense, and/or sell copies of the Software, and to
9
 * permit persons to whom the Software is furnished to do so, subject to
10
 * the following conditions:
11
 *
12
 * The above copyright notice and this permission notice shall be
13
 * included in all copies or substantial portions of the Software.
14
 *
15
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
16
 * EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
17
 * MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
18
 * NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
19
 * LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
20
 * OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
21
 * WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
22
 *
23
 */
24
25
/**
26
 * @fileoverview The WaveFileCreator class.
27
 * @see https://github.com/rochars/wavefile
28
 */
29
30
/** @module wavefile */
31
32
import WaveFileParser from './wavefile-parser';
33
import interleave from './interleave';
34
import dwChannelMask from './dw-channel-mask';
35
import validateNumChannels from './validate-num-channels'; 
36
import validateSampleRate from './validate-sample-rate';
37
import {packArrayTo, packTo, unpack} from 'byte-data';
38
39
/**
40
 * A class to read, write and create wav files.
41
 * @extends WaveFileParser
42
 */
43
export default class WaveFileCreator extends WaveFileParser {
44
45
  constructor() {
46
    super();
47
    /**
48
     * The bit depth code according to the samples.
49
     * @type {string}
50
     */
51
    this.bitDepth = '0';
52
    /**
53
     * @type {!Object}
54
     * @protected
55
     */
56
    this.dataType = {};
57
    /**
58
     * Audio formats.
59
     * Formats not listed here should be set to 65534,
60
     * the code for WAVE_FORMAT_EXTENSIBLE
61
     * @enum {number}
62
     * @protected
63
     */
64
    this.WAV_AUDIO_FORMATS = {
65
      '4': 17,
66
      '8': 1,
67
      '8a': 6,
68
      '8m': 7,
69
      '16': 1,
70
      '24': 1,
71
      '32': 1,
72
      '32f': 3,
73
      '64': 3
74
    };
75
  }
76
77
  /**
78
   * Set up the WaveFileCreator object based on the arguments passed.
79
   * @param {number} numChannels The number of channels
80
   *    (Integer numbers: 1 for mono, 2 stereo and so on).
81
   * @param {number} sampleRate The sample rate.
82
   *    Integer numbers like 8000, 44100, 48000, 96000, 192000.
83
   * @param {string} bitDepthCode The audio bit depth code.
84
   *    One of '4', '8', '8a', '8m', '16', '24', '32', '32f', '64'
85
   *    or any value between '8' and '32' (like '12').
86
   * @param {!Array<number>|!Array<!Array<number>>|!TypedArray} samples
87
   *    The samples. Must be in the correct range according to the bit depth.
88
   * @param {?Object} options Optional. Used to force the container
89
   *    as RIFX with {'container': 'RIFX'}
90
   * @throws {Error} If any argument does not meet the criteria.
91
   */
92
  fromScratch(numChannels, sampleRate, bitDepthCode, samples, options={}) {
93
    if (!options.container) {
94
      options.container = 'RIFF';
95
    }
96
    this.container = options.container;
97
    this.bitDepth = bitDepthCode;
98
    samples = interleave(samples);
99
    this.updateDataType_();
100
    /** @type {number} */
101
    let numBytes = this.dataType.bits / 8;
102
    this.data.samples = new Uint8Array(samples.length * numBytes);
103
    packArrayTo(samples, this.dataType, this.data.samples);
104
    this.clearHeader();
105
    this.makeWavHeader_(
106
      bitDepthCode, numChannels, sampleRate,
107
      numBytes, this.data.samples.length, options);
108
    this.data.chunkId = 'data';
109
    this.data.chunkSize = this.data.samples.length;
110
    this.validateWavHeader_();
111
  }
112
113
  /**
114
   * Set up the WaveFileParser object from a byte buffer.
115
   * @param {!Uint8Array} wavBuffer The buffer.
116
   * @param {boolean=} samples True if the samples should be loaded.
117
   * @throws {Error} If container is not RIFF, RIFX or RF64.
118
   * @throws {Error} If format is not WAVE.
119
   * @throws {Error} If no 'fmt ' chunk is found.
120
   * @throws {Error} If no 'data' chunk is found.
121
   */
122
  fromBuffer(wavBuffer, samples=true) {
123
    super.fromBuffer(wavBuffer, samples);
124
    this.bitDepthFromFmt_();
125
    this.updateDataType_();
126
  }
127
128
  /**
129
   * Return a byte buffer representig the WaveFileParser object as a .wav file.
130
   * The return value of this method can be written straight to disk.
131
   * @return {!Uint8Array} A wav file.
132
   * @throws {Error} If bit depth is invalid.
133
   * @throws {Error} If the number of channels is invalid.
134
   * @throws {Error} If the sample rate is invalid.
135
   */
136
  toBuffer() {
137
    this.validateWavHeader_();
138
    return super.toBuffer();
139
  }
140
141
    /**
142
   * Return the sample at a given index.
143
   * @param {number} index The sample index.
144
   * @return {number} The sample.
145
   * @throws {Error} If the sample index is off range.
146
   */
147
  getSample(index) {
148
    index = index * (this.dataType.bits / 8);
149
    if (index + this.dataType.bits / 8 > this.data.samples.length) {
150
      throw new Error('Range error');
151
    }
152
    return unpack(
153
      this.data.samples.slice(index, index + this.dataType.bits / 8),
154
      this.dataType);
155
  }
156
157
  /**
158
   * Set the sample at a given index.
159
   * @param {number} index The sample index.
160
   * @param {number} sample The sample.
161
   * @throws {Error} If the sample index is off range.
162
   */
163
  setSample(index, sample) {
164
    index = index * (this.dataType.bits / 8);
165
    if (index + this.dataType.bits / 8 > this.data.samples.length) {
166
      throw new Error('Range error');
167
    }
168
    packTo(sample, this.dataType, this.data.samples, index);
169
  }
170
171
  /**
172
   * Define the header of a wav file.
173
   * @param {string} bitDepthCode The audio bit depth
174
   * @param {number} numChannels The number of channels
175
   * @param {number} sampleRate The sample rate.
176
   * @param {number} numBytes The number of bytes each sample use.
177
   * @param {number} samplesLength The length of the samples in bytes.
178
   * @param {!Object} options The extra options, like container defintion.
179
   * @private
180
   */
181
  makeWavHeader_(
182
    bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options) {
183
    if (bitDepthCode == '4') {
184
      this.createADPCMHeader_(
185
        bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options);
186
187
    } else if (bitDepthCode == '8a' || bitDepthCode == '8m') {
188
      this.createALawMulawHeader_(
189
        bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options);
190
191
    } else if(Object.keys(this.WAV_AUDIO_FORMATS).indexOf(bitDepthCode) == -1 ||
192
        numChannels > 2) {
193
      this.createExtensibleHeader_(
194
        bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options);
195
196
    } else {
197
      this.createPCMHeader_(
198
        bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options);      
199
    }
200
  }
201
202
  /**
203
   * Create the header of a linear PCM wave file.
204
   * @param {string} bitDepthCode The audio bit depth
205
   * @param {number} numChannels The number of channels
206
   * @param {number} sampleRate The sample rate.
207
   * @param {number} numBytes The number of bytes each sample use.
208
   * @param {number} samplesLength The length of the samples in bytes.
209
   * @param {!Object} options The extra options, like container defintion.
210
   * @private
211
   */
212
  createPCMHeader_(
213
    bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options) {
214
    this.container = options.container;
215
    this.chunkSize = 36 + samplesLength;
216
    this.format = 'WAVE';
217
    this.bitDepth = bitDepthCode;
218
    this.fmt = {
219
      chunkId: 'fmt ',
220
      chunkSize: 16,
221
      audioFormat: this.WAV_AUDIO_FORMATS[bitDepthCode] || 65534,
222
      numChannels: numChannels,
223
      sampleRate: sampleRate,
224
      byteRate: (numChannels * numBytes) * sampleRate,
225
      blockAlign: numChannels * numBytes,
226
      bitsPerSample: parseInt(bitDepthCode, 10),
227
      cbSize: 0,
228
      validBitsPerSample: 0,
229
      dwChannelMask: 0,
230
      subformat: []
231
    };
232
  }
233
234
  /**
235
   * Create the header of a ADPCM wave file.
236
   * @param {string} bitDepthCode The audio bit depth
237
   * @param {number} numChannels The number of channels
238
   * @param {number} sampleRate The sample rate.
239
   * @param {number} numBytes The number of bytes each sample use.
240
   * @param {number} samplesLength The length of the samples in bytes.
241
   * @param {!Object} options The extra options, like container defintion.
242
   * @private
243
   */
244
  createADPCMHeader_(
245
    bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options) {
246
    this.createPCMHeader_(
247
      bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options);
248
    this.chunkSize = 40 + samplesLength;
249
    this.fmt.chunkSize = 20;
250
    this.fmt.byteRate = 4055;
251
    this.fmt.blockAlign = 256;
252
    this.fmt.bitsPerSample = 4;
253
    this.fmt.cbSize = 2;
254
    this.fmt.validBitsPerSample = 505;
255
    this.fact = {
256
      chunkId: 'fact',
257
      chunkSize: 4,
258
      dwSampleLength: samplesLength * 2
259
    };
260
  }
261
262
  /**
263
   * Create the header of WAVE_FORMAT_EXTENSIBLE file.
264
   * @param {string} bitDepthCode The audio bit depth
265
   * @param {number} numChannels The number of channels
266
   * @param {number} sampleRate The sample rate.
267
   * @param {number} numBytes The number of bytes each sample use.
268
   * @param {number} samplesLength The length of the samples in bytes.
269
   * @param {!Object} options The extra options, like container defintion.
270
   * @private
271
   */
272
  createExtensibleHeader_(
273
      bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options) {
274
    this.createPCMHeader_(
275
      bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options);
276
    this.chunkSize = 36 + 24 + samplesLength;
277
    this.fmt.chunkSize = 40;
278
    this.fmt.bitsPerSample = ((parseInt(bitDepthCode, 10) - 1) | 7) + 1;
279
    this.fmt.cbSize = 22;
280
    this.fmt.validBitsPerSample = parseInt(bitDepthCode, 10);
281
    this.fmt.dwChannelMask = dwChannelMask(numChannels);
282
    // subformat 128-bit GUID as 4 32-bit values
283
    // only supports uncompressed integer PCM samples
284
    this.fmt.subformat = [1, 1048576, 2852126848, 1905997824];
285
  }
286
287
  /**
288
   * Create the header of mu-Law and A-Law wave files.
289
   * @param {string} bitDepthCode The audio bit depth
290
   * @param {number} numChannels The number of channels
291
   * @param {number} sampleRate The sample rate.
292
   * @param {number} numBytes The number of bytes each sample use.
293
   * @param {number} samplesLength The length of the samples in bytes.
294
   * @param {!Object} options The extra options, like container defintion.
295
   * @private
296
   */
297
  createALawMulawHeader_(
298
      bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options) {
299
    this.createPCMHeader_(
300
      bitDepthCode, numChannels, sampleRate, numBytes, samplesLength, options);
301
    this.chunkSize = 40 + samplesLength;
302
    this.fmt.chunkSize = 20;
303
    this.fmt.cbSize = 2;
304
    this.fmt.validBitsPerSample = 8;
305
    this.fact = {
306
      chunkId: 'fact',
307
      chunkSize: 4,
308
      dwSampleLength: samplesLength
309
    };
310
  }
311
312
  /**
313
   * Set the string code of the bit depth based on the 'fmt ' chunk.
314
   * @private
315
   */
316
  bitDepthFromFmt_() {
317
    if (this.fmt.audioFormat === 3 && this.fmt.bitsPerSample === 32) {
318
      this.bitDepth = '32f';
319
    } else if (this.fmt.audioFormat === 6) {
320
      this.bitDepth = '8a';
321
    } else if (this.fmt.audioFormat === 7) {
322
      this.bitDepth = '8m';
323
    } else {
324
      this.bitDepth = this.fmt.bitsPerSample.toString();
325
    }
326
  }
327
328
  /**
329
   * Validate the bit depth.
330
   * @return {boolean} True is the bit depth is valid.
331
   * @throws {Error} If bit depth is invalid.
332
   * @private
333
   */
334
  validateBitDepth_() {
335
    if (!this.WAV_AUDIO_FORMATS[this.bitDepth]) {
336
      if (parseInt(this.bitDepth, 10) > 8 &&
337
          parseInt(this.bitDepth, 10) < 54) {
338
        return true;
339
      }
340
      throw new Error('Invalid bit depth.');
341
    }
342
    return true;
343
  }
344
345
  /**
346
   * Update the type definition used to read and write the samples.
347
   * @private
348
   */
349
  updateDataType_() {
350
    this.dataType = {
351
      bits: ((parseInt(this.bitDepth, 10) - 1) | 7) + 1,
352
      fp: this.bitDepth == '32f' || this.bitDepth == '64',
353
      signed: this.bitDepth != '8',
354
      be: this.container == 'RIFX'
355
    };
356
    if (['4', '8a', '8m'].indexOf(this.bitDepth) > -1 ) {
357
      this.dataType.bits = 8;
358
      this.dataType.signed = false;
359
    }
360
  }
361
362
  /**
363
   * Validate the header of the file.
364
   * @throws {Error} If bit depth is invalid.
365
   * @throws {Error} If the number of channels is invalid.
366
   * @throws {Error} If the sample rate is invalid.
367
   * @ignore
368
   * @private
369
   */
370
  validateWavHeader_() {
371
    this.validateBitDepth_();
372
    if (!validateNumChannels(this.fmt.numChannels, this.fmt.bitsPerSample)) {
373
      throw new Error('Invalid number of channels.');
374
    }
375
    if (!validateSampleRate(
376
        this.fmt.numChannels, this.fmt.bitsPerSample, this.fmt.sampleRate)) {
377
      throw new Error('Invalid sample rate.');
378
    }
379
  }
380
}
381